#ifndef RENDERTARGET_H
#define RENDERTARGET_H

#include "RenderContex.h"
#include "GLError.h"

#define GBUFFER_MAX_NUMBER_TEXTURES 5

// A macro to disallow the copy constructor and operator= functions 
// This should be used in the private: declarations for a class
#define    DISALLOW_COPY_AND_ASSIGN(TypeName) \
    TypeName(const TypeName&);                \
    TypeName& operator=(const TypeName&);

class RenderTexture
{
public:
	RenderTexture() {}

	~RenderTexture()
	{
		if (_texture != 0)
		{
			glDeleteTextures(1, &_texture);
			_texture = 0;
		}
	}

	void init(int width = 0, int height = 0, GLenum internal_format = GL_RGBA, GLenum external_format = GL_RGBA, GLenum type = GL_UNSIGNED_BYTE)
	{
		_width = width;
		_height = height; 
		if (_width == 0) _width = RenderContex::window_width;
		if (_height == 0) _height = RenderContex::window_height;
		_internal_format = internal_format;
		_external_format = external_format;
		_type = type;
        
		glGenTextures(1, &_texture);
		glBindTexture(GL_TEXTURE_2D, _texture);
		glTexImage2D(GL_TEXTURE_2D, 0, internal_format, _width, _height, 0, external_format, type, 0);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glBindTexture(GL_TEXTURE_2D, 0);
        
        check_gl_error();
	}

	void resize(int width, int height)
	{
		_width = width;
		_height = height;
		glBindTexture(GL_TEXTURE_2D, _texture);
		glTexImage2D(GL_TEXTURE_2D, 0, _internal_format, _width, _height, 0, _external_format, _type, 0);
		glBindTexture(GL_TEXTURE_2D, 0);
	}

	void render_to_screen() const;

	operator GLuint() const
	{
		return _texture;
	}

	GLuint get_render_texture() const
	{
		return _texture;
	}

	int width() const { return _width; }
	int height() const { return _height; }

private:
	GLuint _texture = 0;

	int _width;
	int _height;

	// texture format, parms in glTexImage2D()
	GLenum _internal_format;
	GLenum _external_format;
	GLenum _type;

	DISALLOW_COPY_AND_ASSIGN(RenderTexture)
};


class DepthStencil
{
public:
	DepthStencil() {}

	~DepthStencil()
	{
		if (_depth_texture != 0)
		{
			glDeleteTextures(1, &_depth_texture);
			_depth_texture = 0;
		}
	}

	void init(int width = 0, int height = 0)
	{
		_width = width; _height = height;
		if (_width == 0) _width = RenderContex::window_width;
		if (_height == 0) _height = RenderContex::window_height;
		//int w = RenderContex::window_width;
		//int h = RenderContex::window_height;

		glGenTextures(1, &_depth_texture);
		glBindTexture(GL_TEXTURE_2D, _depth_texture);
        check_gl_error();
		glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, _width, _height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
        check_gl_error();
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
		//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glBindTexture(GL_TEXTURE_2D, 0);
        
        check_gl_error();
	}

	void resize(const int width, const int height)
	{
		_width = width;
		_height = height;
		glBindTexture(GL_TEXTURE_2D, _depth_texture);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, _width, _height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
		glBindTexture(GL_TEXTURE_2D, 0);
	}

    void render_to_screen() const;
    
	GLuint get_depth_stencil_texture() const
	{
		return _depth_texture;
	}

private:
	GLuint _depth_texture;
	int _width;
	int _height;

	DISALLOW_COPY_AND_ASSIGN(DepthStencil)
};


class RenderTarget
{
public:
	RenderTarget() {}

	virtual ~RenderTarget()
	{
		glDeleteFramebuffers(1, &_fbo);
	}
    
    static void static_init()
    {
        glGetIntegerv(GL_FRAMEBUFFER_BINDING, &default_framebuffer);
    }

	virtual void init(const int width = 0, const int height = 0)
	{
		_width = width; _height = height;
		if (_width == 0) _width = RenderContex::window_width;
		if (_height == 0) _height = RenderContex::window_height;
		_fbo = 0;
		glGenFramebuffers(1, &_fbo);
        check_gl_error();
	}

	void resize(const int width, const int height)
	{
		_width = width;
		_height = height;
	}

	void bind()
	{
		assert(_fbo != 0);
		glBindFramebuffer(GL_FRAMEBUFFER, _fbo);
		//glViewport(0, 0, _width, _height);
	}

	void unbind()
	{
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
	}

	void bind_and_set_depth_stencil(const DepthStencil * depth_stencil)
	{
		bind();
		if (nullptr == depth_stencil)
		{
			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
			return;
		}
        check_gl_error();
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depth_stencil->get_depth_stencil_texture(), 0);
		//glBindFramebuffer(GL_FRAMEBUFFER, 0);
        
        check_gl_error();
		check_fbo_status();
	}

	void bind_and_set_render_texture(const RenderTexture * render_texture)
	{
		bind();
		if (nullptr == render_texture)
		{
			glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
			for (int i = 1; i < _texture_count; i++)
			{
				glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, 0, 0);
			}
			_texture_count = 0;
			return;
		}

		glBindFramebuffer(GL_FRAMEBUFFER, _fbo);
		_texture_count = 1;
		glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_texture->get_render_texture(), 0);

		static GLenum draw_buffers[] = { GL_COLOR_ATTACHMENT0 };
		glDrawBuffers(1, draw_buffers);

		check_fbo_status();
        check_gl_error();
	}

	void bind_and_set_render_texture(const int size, RenderTexture ** render_textures)
	{
		glBindFramebuffer(GL_FRAMEBUFFER, _fbo);
		_texture_count = size;
		assert(1 <= _texture_count && _texture_count <= GBUFFER_MAX_NUMBER_TEXTURES);
		for (int i = 0; i < _texture_count; i++)
		{
			glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, render_textures[i]->get_render_texture(), 0);
		}
		static GLenum draw_buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, GL_COLOR_ATTACHMENT4, GL_COLOR_ATTACHMENT5 };
		glDrawBuffers(_texture_count, draw_buffers);

		check_fbo_status();
        check_gl_error();
	}

	static void check_fbo_status()
	{
		GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
		if (Status != GL_FRAMEBUFFER_COMPLETE)
		{
			printf("[ERROR] Frame Buffer error, status: 0x%x\n", Status);
		}
	}
    
    static GLint default_framebuffer;

private:
	int _texture_count = 0;
	GLuint _fbo = 0;

	int _width;
	int _height;
};


class ShadowMap : public RenderTarget
{
public:
	ShadowMap() {}

	~ShadowMap()
	{
	}

	static void static_init()
	{
		glGenSamplers(1, &sampler_object_for_shadow_map);
		glSamplerParameteri(sampler_object_for_shadow_map, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glSamplerParameteri(sampler_object_for_shadow_map, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glSamplerParameteri(sampler_object_for_shadow_map, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
		glSamplerParameteri(sampler_object_for_shadow_map, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
		glSamplerParameteri(sampler_object_for_shadow_map, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glSamplerParameteri(sampler_object_for_shadow_map, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glGenSamplers(1, &sampler_object_for_depth_texutre);
		glSamplerParameteri(sampler_object_for_depth_texutre, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glSamplerParameteri(sampler_object_for_depth_texutre, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		//glSamplerParameteri(sampler_object_for_depth_texutre, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
		//glSamplerParameteri(sampler_object_for_depth_texutre, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
		glSamplerParameteri(sampler_object_for_depth_texutre, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glSamplerParameteri(sampler_object_for_depth_texutre, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	}

	virtual void init()
	{
		RenderTarget::init(SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);
		_depth_stencil.init(SHADOW_MAP_SIZE, SHADOW_MAP_SIZE);
		bind_and_set_depth_stencil(&_depth_stencil);
        check_gl_error();
	}

	void render_depth_to_screen();

	GLuint get_depth_texture()
	{
		return _depth_stencil.get_depth_stencil_texture();
	}

	static glm::mat4 getViewProjectionTextureMatrix(glm::mat4 const & view, glm::mat4 const & projection)
	{
		static mat4 biasMatrix(
			0.5, 0.0, 0.0, 0.0,
			0.0, 0.5, 0.0, 0.0,
			0.0, 0.0, 0.5, 0.0,
			0.5, 0.5, 0.5, 1.0
			);
		//biasMatrix = mat4(1.0f);
		return biasMatrix * projection * view;
		//return projection * view;
	}
public:
	const static int SHADOW_MAP_SIZE = 2048;
	static GLuint sampler_object_for_shadow_map;
	static GLuint sampler_object_for_depth_texutre;

private:
	DepthStencil _depth_stencil;

	DISALLOW_COPY_AND_ASSIGN(ShadowMap)
};

#endif
